/*
 * MAIN.C
 *
 * Main entry point code to convert and process VT100/ANSI escape sequences.
 *
 * Copyright (c) 2015 Malcolm J. Smith
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "cvtvt.h"

#define MAX_PIPE_SIZE (1024*1024)

int memspn(CHAR * str, ULONG length, CHAR * chars)
{
    DWORD len = 0;
    DWORD i;

    for (len = 0; len < length; len++) {
        for (i = 0; chars[i] != '\0'; i++) {
            if (str[len] == chars[i]) {
                break;
            }
        }
        if (chars[i] == '\0') {
            return len;
        }
    }

    return len;
}

int memcspn(CHAR * str, ULONG length, CHAR * match)
{
    DWORD len = 0;
    DWORD i;

    for (len = 0; len < length; len++) {
        for (i = 0; match[i] != '\0'; i++) {
            if (str[len] == match[i]) {
                return len;
            }
        }
    }

    return len;
}

int memtoi(CHAR * str, ULONG length)
{
    int ret = 0;
    while (length > 0 && *str >= '0' && *str <= '9') {
        ret *= 10;
        ret += *str - '0';
        str++;
        length--;
    }
    return ret;
}

LPSTR strUsage =
        "Usage:\n"
        "  cvtvt [Options] [-log filename] [-exec binary|filename]\n"
        "\n"
        " Options include:\n"
        "  -exec binary    Run process and pipe its output into cvtvt\n"
        "  -help           Display this usage text\n"
        "  -html4          Generate output with FONT tags (no backgrounds)\n"
        "  -html5          Generate output with CSS\n"
        "  -log filename   Replicate input to filename\n"
        "  -text           Generate output as plain text\n"
        "  -win32          Convert to native Win32\n"
        "\n"
        " If filename and exec are not specified, operates on standard"
        " input.\n";

BOOL Usage()
{
    ULONG BytesTransferred;
    CHAR Line[80];
    ULONG CVer;

#ifdef _MSC_VER
    CVer = _MSC_VER;
#else
    CVer = 0;
#endif

    sprintf_s(Line, sizeof(Line), "Cvtvt version %i.%i, compiled %s, %s, C version %i.%02i\n\n",
            CVTVT_VER_MAJOR,
            CVTVT_VER_MINOR,
            __DATE__,
#ifdef MINICRT
            "Minicrt",
#else
            "Msvcrt",
#endif
            CVer / 100,
            CVer % 100);

    WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), Line, strlen(Line), &BytesTransferred, NULL);
    WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), strUsage, strlen(strUsage), &BytesTransferred, NULL);
    return TRUE;
}

DWORD WINAPI
InputPumpThread(LPVOID Context)
{
    HANDLE hOutput = (HANDLE)Context;
    UCHAR StupidBuffer[256];
    DWORD BytesRead;
    DWORD BytesWritten;

    SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT);

    while (ReadFile(GetStdHandle(STD_INPUT_HANDLE), StupidBuffer, sizeof(StupidBuffer), &BytesRead, NULL)) {
        if (!WriteFile(hOutput, StupidBuffer, BytesRead, &BytesWritten, NULL)) {
            break;
        }
    }

    return 0;
}

int __cdecl
main (
    int argc,
    char* argv[]
    )
{
    HANDLE hLog = INVALID_HANDLE_VALUE;
    HANDLE hSource = INVALID_HANDLE_VALUE;
    HANDLE hOutput = INVALID_HANDLE_VALUE;
    HANDLE hControl = INVALID_HANDLE_VALUE;
    
    DWORD  BufferSize;
    HANDLE hMem;
    PUCHAR pMem;
    ULONG  ChunkSize;
    ULONG  CurrentOffset;
    ULONG  PreviouslyConsumed = 0;
    LPSTR  CurrentPoint;

    CHAR   VtEscape[] = {27, '\0'};
    LPSTR  Filename = NULL;
    LPSTR  LogFile = NULL;
    CALLBACK_FUNCTIONS Callbacks;

    BOOLEAN StreamStarted = FALSE;
    BOOLEAN ExecMode = FALSE;
    BOOLEAN DisplayUsage = FALSE;
    BOOLEAN ArgParsed = FALSE;

    Html4SetFunctions(&Callbacks);

    //
    //  Parse arguments
    //

    for (CurrentOffset = 1; CurrentOffset < (DWORD)argc; CurrentOffset++) {
        if (argv[CurrentOffset][0] == '-' || argv[CurrentOffset][0] == '/') {
            LPSTR Arg = &argv[CurrentOffset][1];

            ArgParsed = FALSE;

            if (stricmp(Arg, "?") == 0) {
                ArgParsed = TRUE;
                DisplayUsage = TRUE;
            } else if (stricmp(Arg, "exec") == 0) {
                ArgParsed = TRUE;
                ExecMode = TRUE;
            } else if (stricmp(Arg, "help") == 0) {
                ArgParsed = TRUE;
                DisplayUsage = TRUE;
            } else if (stricmp(Arg, "html4") == 0) {
                ArgParsed = TRUE;
                Html4SetFunctions(&Callbacks);
            } else if (stricmp(Arg, "html5") == 0) {
                ArgParsed = TRUE;
                Html5SetFunctions(&Callbacks);
            } else if (stricmp(Arg, "log") == 0) {
                ArgParsed = TRUE;
                LogFile = argv[CurrentOffset + 1];
                CurrentOffset++;
            } else if (stricmp(Arg, "text") == 0) {
                ArgParsed = TRUE;
                TextSetFunctions(&Callbacks);
            } else if (stricmp(Arg, "win32") == 0) {
                ArgParsed = TRUE;
                Win32SetFunctions(&Callbacks);
            }

            if (!ArgParsed) {
                DisplayUsage = TRUE;
            }

        } else {
            Filename = argv[CurrentOffset];
        }
    }

    //
    //  As a bit of a hack, if the input is stdin and stdin is to
    //  a console (as opposed to a pipe or file) assume the user isn't
    //  sure how to run this program and help them along.
    //

    if (Filename == NULL) {
        DWORD FileType = GetFileType(GetStdHandle(STD_INPUT_HANDLE));
        FileType = FileType & ~(FILE_TYPE_REMOTE);
        if (FileType == FILE_TYPE_CHAR) {
            DisplayUsage = TRUE;
        }
    }

    if (DisplayUsage) {
        Usage();
        return EXIT_FAILURE;
    }

    //
    //  If we have a file name, read it; otherwise read from stdin
    //

    if (ExecMode) {
        if (Filename == NULL) {
            Usage();
            return EXIT_FAILURE;
        } else {
            HANDLE hProcessInput;
            HANDLE hProcessOutput;
            STARTUPINFO StartupInfo;
            PROCESS_INFORMATION ProcessInfo;
            SECURITY_ATTRIBUTES SecurityAttributes;
            CHAR szTermVar[256];
            DWORD dwConsoleMode;

            ZeroMemory(&StartupInfo, sizeof(StartupInfo));
            ZeroMemory(&ProcessInfo, sizeof(ProcessInfo));
            ZeroMemory(&SecurityAttributes, sizeof(SecurityAttributes));

            SecurityAttributes.nLength = sizeof(SecurityAttributes);
            SecurityAttributes.bInheritHandle = TRUE;

            CreatePipe(&hProcessInput,
                       &hControl,
                       &SecurityAttributes,
                       2048);

            CreatePipe(&hSource,
                       &hProcessOutput,
                       &SecurityAttributes,
                       2048);

            GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &dwConsoleMode);
            sprintf(szTermVar, "color;windowsize;extendedchars%s", dwConsoleMode&ENABLE_WRAP_AT_EOL_OUTPUT?";autolinewrap":"");
            SetEnvironmentVariable("NTTERM", szTermVar);

            SetHandleInformation(hSource, HANDLE_FLAG_INHERIT, 0);
            SetHandleInformation(hControl, HANDLE_FLAG_INHERIT, 0);

            SetHandleInformation(hProcessInput, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
            SetHandleInformation(hProcessOutput, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);

            StartupInfo.dwFlags = STARTF_USESTDHANDLES;
            StartupInfo.hStdInput = hProcessInput;
            StartupInfo.hStdOutput = hProcessOutput;
            StartupInfo.hStdError = hProcessOutput;

            SetEnvironmentVariable("PROMPT", "$e[31;1m$p$e[37m$g");

            if (!CreateProcess(Filename,
                               Filename,
                               NULL,
                               NULL,
                               TRUE,
                               0,
                               NULL,
                               NULL,
                               &StartupInfo,
                               &ProcessInfo)) {
                printf("Could not launch process, error %i\n", GetLastError());
                return EXIT_FAILURE;
            }

            CloseHandle(hProcessInput);
            CloseHandle(hProcessOutput);

            CloseHandle(ProcessInfo.hProcess);
            CloseHandle(ProcessInfo.hThread);

            CreateThread(NULL, 0, InputPumpThread, hControl, 0, NULL);
        }
    } else if (Filename != NULL) {
        hSource = CreateFile(Filename, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL);
        if (hSource == INVALID_HANDLE_VALUE) {
            printf("Could not open file, error %i\n", GetLastError());
            return EXIT_FAILURE;
        }
    } else {
        hSource = GetStdHandle(STD_INPUT_HANDLE);
    }

    if (LogFile != NULL) {
        hLog = CreateFile(LogFile, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_DELETE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,NULL);
        if (hLog == INVALID_HANDLE_VALUE) {
            printf("Could not open log file, error %i\n", GetLastError());
            return EXIT_FAILURE;
        }
    }

    hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    BufferSize = 50;
    
    hMem = GlobalAlloc(GMEM_MOVEABLE|GMEM_DDESHARE, BufferSize);
    if (hMem == NULL) {
        printf("Could not allocate memory, error %i\n", GetLastError());
        return EXIT_FAILURE;
    }

    pMem = GlobalLock(hMem);

    CurrentOffset = 0;

    while (TRUE) {

        if (!ReadFile(hSource, pMem + CurrentOffset, BufferSize - CurrentOffset, &ChunkSize, NULL)) {
            break;
        }

        //
        //  If we didn't read anything, we're at the end.
        //

        if (ChunkSize == 0) {
            break;
        }

        //
        //  Pretend we read as much as this operation and any previous carryover
        //

        ChunkSize += CurrentOffset;

        //
        //  Start producing HTML
        //

        if (!StreamStarted) {
            Callbacks.InitializeStream(hOutput, hControl);
            StreamStarted = TRUE;
        }

        CurrentPoint = pMem;
        CurrentOffset = memcspn(CurrentPoint, ChunkSize, VtEscape);
        PreviouslyConsumed = 0;
    
        while (TRUE) {
    
            //
            //  If we have any text, perform required processing and output the text
            //
    
            if (CurrentOffset > 0) {
                Callbacks.ProcessAndOutputText(hOutput, hControl, CurrentPoint, CurrentOffset);
    
                CurrentPoint = CurrentPoint + CurrentOffset;
                PreviouslyConsumed += CurrentOffset;
            }

            if (PreviouslyConsumed >= ChunkSize) {
                break;
            }
    
            //
            //  Locate VT100 escapes and process those
            //
    
            if (*CurrentPoint == 27) {

                if (PreviouslyConsumed + 3 < ChunkSize &&
                    CurrentPoint[1] == '[') {
        
                    DWORD EndOfEscape;
                    EndOfEscape = memspn(&CurrentPoint[2], ChunkSize - PreviouslyConsumed - 2, "0123456789;");

                    //
                    //  If our buffer is full and we still have an incomplete escape,
                    //  there's no more processing we can perform.  This input is
                    //  bogus.
                    //

                    if (PreviouslyConsumed == 0 && EndOfEscape == ChunkSize - 2) {
                        return FALSE;
                    }

                    //
                    //  If we have an incomplete escape for this chunk, stop
                    //  processing here, loop back and read more data.
                    //

                    if (EndOfEscape == ChunkSize - PreviouslyConsumed - 2) {
                        break;
                    }

                    Callbacks.ProcessAndOutputEscape(hOutput, hControl, CurrentPoint, EndOfEscape + 3);
                    CurrentPoint = CurrentPoint + EndOfEscape + 3;
                    PreviouslyConsumed += EndOfEscape + 3;
                } else {
                    break;
                }
            }

            if (PreviouslyConsumed >= ChunkSize) {
                break;
            }
    
            CurrentOffset = memcspn(CurrentPoint, ChunkSize - PreviouslyConsumed, VtEscape);
        }

        //
        //  If we're logging, write out what we read
        //

        if (hLog != INVALID_HANDLE_VALUE) {
            ULONG BytesWritten;
            WriteFile(hLog, pMem, PreviouslyConsumed, &BytesWritten, NULL);
        }

        if (PreviouslyConsumed >= ChunkSize) {
            CurrentOffset = 0;
        } else {
            memmove(pMem, pMem + PreviouslyConsumed, ChunkSize - PreviouslyConsumed);
            CurrentOffset = ChunkSize - PreviouslyConsumed;
        }
    }

    if (StreamStarted) {
        Callbacks.EndStream(hOutput, hControl);
    }

    GlobalUnlock(hMem);
    GlobalFree(hMem);
    if (Filename != NULL) {
        CloseHandle(hSource);
    }
    if (hControl != INVALID_HANDLE_VALUE) {
        CloseHandle(hControl);
    }
    if (hLog != INVALID_HANDLE_VALUE) {
        CloseHandle(hLog);
    }
    return TRUE;
}

// vim:sw=4:ts=4:et:
